package com.rmi;

import com.google.gson.Gson;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.SocketException;
import java.util.HashMap;
import java.util.Map;
import javassist.util.proxy.MethodFilter;
import javassist.util.proxy.ProxyFactory;

/**
 * @author Dmitry
 */
public class JsonRmi {

    private ServerSocket serverSoket;
    private final Map<String, Object> handlerCache = new HashMap<>();
    private final Gson gson;
    private final Map<Object, RmiMethodHandler> objToMethodHandler = new HashMap<>();
    private MessageEvent messageEvent;

    private static JsonRmi instance;

    public static JsonRmi get() {
        if (instance == null) {
            instance = new JsonRmi();
        }
        return instance;
    }

    public void setMessageEvent(MessageEvent messageEvent) {
        this.messageEvent = messageEvent;
    }

    private JsonRmi() {
        gson = new Gson();
    }

    public void releaseRmiObject(Object obj) {
        RmiMethodHandler mh = objToMethodHandler.get(obj);
        if (mh != null) {
            mh.closeSocket();
        }
    }

    public void releaseRmiObjects() {
        for (RmiMethodHandler mh : objToMethodHandler.values()) {
            mh.closeSocket();
        }
    }

    public <T> T constructRmiObject(InetAddress address, int port, Class<T> clazz) {
        return constructRmiObject(address, port, clazz, null);
    }

    public <T> T constructRmiObject(InetAddress address, int port, Class<T> clazz, String id) {
        ProxyFactory factory = new ProxyFactory();
        factory.setSuperclass(clazz);
        factory.setFilter(new MethodFilter() {

            @Override
            public boolean isHandled(Method m) {
                return !(m.getName().equals("toString")
                        || m.getName().equals("hashCode")
                        || m.getName().equals("clone")
                        || m.getName().equals("notify")
                        || m.getName().equals("notifyAll")
                        || m.getName().equals("wait")
                        || m.getName().equals("waitAll"));
            }
        });
        RmiMethodHandler mh = new RmiMethodHandler(clazz, id, address, port);
        factory.setHandler(mh);
        try {
            T value = (T) factory.create(new Class[0], new Object[0]);
            objToMethodHandler.put(value, mh);
            return value;
        } catch (NoSuchMethodException | IllegalArgumentException | InstantiationException | IllegalAccessException | InvocationTargetException ex) {
            throw new RuntimeException(ex);
        }
    }

    public void shutdown() {
        try {
            serverSoket.close();
        } catch (IOException ex) {
            throw new RuntimeException(ex);
        }
    }

    public boolean isServerEnabled() {
        return serverSoket != null && !serverSoket.isClosed();
    }

    public void initRmiServer(final int port, boolean block) {
        if (block) {
            serverSocketInit(port);
        } else {
            Thread thread = new Thread() {

                @Override
                public void run() {
                    Thread.setDefaultUncaughtExceptionHandler(new UncaughtExceptionHandler() {

                        @Override
                        public void uncaughtException(Thread t, Throwable e) {
                            e.printStackTrace();
                            if (messageEvent != null) {
                                messageEvent.message(e.getMessage());
                            }
                        }
                    });
                    serverSocketInit(port);
                }
            };
            thread.start();
        }
    }

    private void serverSocketInit(int port) {
        try {
            serverSoket = new ServerSocket(port);
        } catch (IOException ex) {
            throw new RuntimeException(ex);
        }
        while (true) {
            try {
                final Socket socket = serverSoket.accept();
                socket.setSoTimeout(60000);
                final InputStream istream = new BufferedInputStream(socket.getInputStream());
                final OutputStream ostream = new BufferedOutputStream(socket.getOutputStream());
                Thread thread = new Thread() {

                    @Override
                    public void run() {
                        Thread.setDefaultUncaughtExceptionHandler(new UncaughtExceptionHandler() {

                            @Override
                            public void uncaughtException(Thread t, Throwable e) {
                                e.printStackTrace();
                                if (messageEvent != null) {
                                    messageEvent.message(e.getMessage());
                                }
                            }
                        });
                        //while (true) {
                        try {
                            processSocket(istream, ostream);
                        } catch (Exception ex) {
                            throw new RuntimeException(ex);
                        }
                        // }
                    }
                };

                thread.start();
            } catch (IOException ex) {
                if (ex instanceof SocketException) {
                    if (serverSoket.isClosed()) {
                        break;
                    }
                }
                throw new RuntimeException(ex);
            }
        }
    }

    public <T> void addRmiHandler(Class<T> clazz, T handler) {
        handlerCache.put(clazz.getName(), handler);
    }

    public <T> void addRmiHandler(Class<T> clazz, String id, T handler) {
        handlerCache.put(clazz.getName() + id, handler);
    }

    protected void processSocket(InputStream istream, OutputStream ostream) throws Exception {
        RmiStream rmiStream = new RmiStream();
        String data = rmiStream.toReadString(istream);
        if (data != null && !data.isEmpty()) {
            String response = processRequest(data);
            if (response != null) {
                rmiStream.writeString(ostream, response);
            }
        }
    }

    protected String processRequest(String request) {
        int index = request.indexOf('|');
        String headerString = request.substring(0, index);
        String data = request.substring(index + 1);
        RmiHeader header = gson.fromJson(headerString, RmiHeader.class);
        RmiHeader.RmiMethod method = header.getRmiMethod();
        String result;
        try {
            result = evaluateRmiMethod(method, data);
        } catch (Exception ex) {
            RmiHeader.RmiException rmiException = new RmiHeader.RmiException();
            if (ex instanceof InvocationTargetException) {
                InvocationTargetException invEx = (InvocationTargetException) ex;
                ex = (Exception) invEx.getTargetException();
            }

            rmiException.setClassName(ex.getClass().getName());
            rmiException.setMessage(ex.getMessage());
            return "exception|" + gson.toJson(rmiException);
        }

        if (data != null) {
            return "data|" + result;
        }

        return null;
    }

    String extractPatemeter(String data, int index, RmiHeader.RmiParameter[] parameters) {
        int length = parameters.length;
        if (index >= length - 1) {
            int offset = parameters[index].getOffset();
            return data.substring(offset);
        }

        int offset = parameters[index].getOffset();
        int offset1 = parameters[index + 1].getOffset();
        return data.substring(offset, offset1);
    }

    private Class getClassFromString(String classString) throws ClassNotFoundException {
        switch (classString) {
            case "int":
                return int.class;
            case "float":
                return float.class;
            case "double":
                return double.class;
            case "boolean":
                return boolean.class;
            case "short":
                return short.class;
            case "long":
                return long.class;
        }
        return Class.forName(classString);
    }

    String evaluateRmiMethod(RmiHeader.RmiMethod rmiMethod, String data) throws Exception {
        Object handler = handlerCache.get(rmiMethod.getClassName());
        if (handler == null) {
            Class clazz = Class.forName(rmiMethod.getClassName());
            handler = clazz.newInstance();
            handlerCache.put(rmiMethod.getClassName(), handler);
        }

        int length = rmiMethod.getParameters().length;
        Class[] parameters = new Class[length];
        Object[] objParameters = new Object[length];
        for (int i = 0; i < length; i++) {
            String param = extractPatemeter(data, i, rmiMethod.getParameters());
            parameters[i] = getClassFromString(rmiMethod.getParameters()[i].getType());
            Object paramValue = gson.fromJson(param, parameters[i]);
            objParameters[i] = paramValue;
        }

        Method method = null;
        try {
            method = handler.getClass().getMethod(rmiMethod.getMethodName(), parameters);
        } catch (NoSuchMethodException noSuchMethodException) {
            throw new JsonRmiException(noSuchMethodException.getMessage());
        }

        Object returnObject = method.invoke(handler, objParameters);
        return gson.toJson(returnObject);
    }

    public static interface MessageEvent {
        public void message(String message);
    }
}
